Skip to content

Error if the calculated reserve would be greater than the channel value#4580

Open
tankyleo wants to merge 2 commits intolightningdevkit:mainfrom
tankyleo:2026-04-reserve-greater-than-channel-value
Open

Error if the calculated reserve would be greater than the channel value#4580
tankyleo wants to merge 2 commits intolightningdevkit:mainfrom
tankyleo:2026-04-reserve-greater-than-channel-value

Conversation

@tankyleo
Copy link
Copy Markdown
Contributor

@tankyleo tankyleo commented Apr 30, 2026

In 0FC channels, capping the reserve to the total value of the channel
allowed a splice initiator to withdraw past their reserve in case the
acceptor had no balance in the channel.

This is because the post-splice value of the channel was equal to the
initiator's post splice balance. Hence, this post splice balance always
matched the reserve, even though the reserve was below the dust limit.

The only thing that prevented the initiator from withdrawing all their
balance was the script dust limit check in
`interactivetxs::NegotiationContext::receive_tx_add_output`.

In case the splice acceptor had any balance in the channel, or there
were HTLCs in the channel, or the channel was not 0FC, the
splice initiator's post-splice balance was always below the full channel
value. Hence when the reserve was capped at the channel value, the
post-splice balance was always below the reserve, and the splice was
rejected.

Also, in `validate_splice_contributions`, to determine the
`counterparty_selected_channel_reserve`, we now read the holder's dust
limit from the context, instead of the current global constant.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 30, 2026

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tankyleo tankyleo force-pushed the 2026-04-reserve-greater-than-channel-value branch from 7d64174 to 06a604d Compare April 30, 2026 01:52
@tankyleo tankyleo self-assigned this Apr 30, 2026
@tankyleo tankyleo moved this to Goal: Merge in Weekly Goals Apr 30, 2026
@tankyleo tankyleo force-pushed the 2026-04-reserve-greater-than-channel-value branch from 06a604d to 9669465 Compare May 6, 2026 20:20
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 83.75000% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.11%. Comparing base (964a84f) to head (53e156a).
⚠️ Report is 46 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 81.15% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4580      +/-   ##
==========================================
- Coverage   86.18%   86.11%   -0.07%     
==========================================
  Files         156      157       +1     
  Lines      108528   108871     +343     
  Branches   108528   108871     +343     
==========================================
+ Hits        93532    93752     +220     
- Misses      12386    12504     +118     
- Partials     2610     2615       +5     
Flag Coverage Δ
tests 86.11% <83.75%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tankyleo tankyleo marked this pull request as ready for review May 7, 2026 05:30
@ldk-reviews-bot ldk-reviews-bot requested a review from joostjager May 7, 2026 05:30
@tankyleo tankyleo requested review from TheBlueMatt and wpaulino and removed request for joostjager May 7, 2026 05:31
Comment thread lightning/src/ln/channel.rs Outdated
== 0,
);
)
.map_err(|()| format!("The post-splice channel value {post_channel_value_sat} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}"))?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The error message references MIN_CHAN_DUST_LIMIT_SATOSHIS but the actual dust limit passed to get_v2_channel_reserve_satoshis on line 2822 is context.holder_dust_limit_satoshis. These can differ (e.g. in anchor channels with higher dust limits, or as demonstrated by the new test which sets holder_dust_limit_satoshis = 10_000). The error message will report an incorrect (lower) value when the holder's dust limit exceeds the minimum.

Suggested change
.map_err(|()| format!("The post-splice channel value {post_channel_value_sat} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}"))?;
.map_err(|()| format!("The post-splice channel value {post_channel_value_sat} is smaller than our dust limit {}", context.holder_dust_limit_satoshis))?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed below

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented May 7, 2026

I've completed a thorough review of every file and hunk in this PR. The changes are well-structured — the reserve functions now properly reject cases where the channel value is below the dust limit or minimum reserve, the cmp::min(channel_value, ...) cap is correctly replaced by early error returns, and the for_splice path correctly uses context.holder_dust_limit_satoshis instead of MIN_CHAN_DUST_LIMIT_SATOSHIS.

No new issues found.

Prior review status: My previous inline comment at channel.rs:2828 appears to have been incorrect — the current code already uses context.holder_dust_limit_satoshis in the error message, matching the value passed to get_v2_channel_reserve_satoshis.

Verification notes:

  • All callers of the now-Result-returning functions handle Err properly via map_err with descriptive messages.
  • The unwrap() calls in tx_builder.rs are inside #[cfg(debug_assertions)] blocks and are safe because channel_value >= dust_limit is guaranteed by the constraint computation for existing channels.
  • The cmp::min(prop, 1_000_000) cap in get_holder_selected_channel_reserve_satoshis matches the documented behavior in ChannelHandshakeConfig.
  • The removal of cmp::min(channel_value, ...) is safe: the early Err checks ensure the reserve never exceeds the channel value on the Ok path, preserving the "no larger than channel_value_satoshis" doc guarantee.
  • The is_0reserve short-circuit after the error check means 0-reserve channels with value below thresholds are now rejected, which is correct and intentional per the PR description.
  • Test visibility for counterparty_dust_limit_satoshis follows the existing #[cfg(test)]/pub(crate) pattern used by holder_dust_limit_satoshis.

@tankyleo tankyleo added this to the 0.3 milestone May 7, 2026
Comment thread lightning/src/sign/tx_builder.rs Outdated
next_splice_out_maximum_sat =
(local_balance_before_fee_msat / 1000).saturating_sub(min_balance_sat);
}
// We only bother to check the local commitment here, the counterparty will check its own commitment.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the thinking here that because the counterparty set a reserve we assume (I guess probably check somewhere) that reserve is high enough to allow for an output? Not entirely sure its worth changing this to make that assumption.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I dropped this diff hunk, see below

tankyleo added 2 commits May 8, 2026 06:02
In 0FC channels, capping the reserve to the total value of the channel
allowed a splice initiator to withdraw past their reserve in case the
acceptor had no balance in the channel.

This is because the post-splice value of the channel was equal to the
initiator's post splice balance. Hence, this post splice balance always
matched the reserve, even though the reserve was below the dust limit.

The only thing that prevented the initiator from withdrawing all their
balance was the script dust limit check in
`interactivetxs::NegotiationContext::receive_tx_add_output`.

In case the splice acceptor had any balance in the channel, or there
were HTLCs in the channel, or the channel was not 0FC, the
splice initiator's post-splice balance was always below the full channel
value. Hence when the reserve was capped at the channel value, the
post-splice balance was always below the reserve, and the splice was
rejected.

Also, in `validate_splice_contributions`, to determine the
`counterparty_selected_channel_reserve`, we now read the holder's dust
limit from the context, instead of the current global constant.
We made the same change to the calculation of the v2 reserve in the
previous commit.
@tankyleo tankyleo force-pushed the 2026-04-reserve-greater-than-channel-value branch from 9669465 to 53e156a Compare May 8, 2026 06:07
@tankyleo tankyleo requested a review from TheBlueMatt May 8, 2026 06:07
@tankyleo
Copy link
Copy Markdown
Contributor Author

tankyleo commented May 8, 2026

Dropped the move of the no_outputs check in tx_builder, and made this diff:

diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index e59855d28..137bdd28f 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -2825,14 +2825,25 @@
                                .expect("counterparty reserve is set")
                                == 0,
                )
-               .map_err(|()| format!("The post-splice channel value {post_channel_value_sat} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}"))?;
-               let their_dust_limit_satoshis = context.counterparty_dust_limit_satoshis;
+               .map_err(|()| {
+                       format!(
+                               "The post-splice channel value {post_channel_value_sat} is smaller \
+                               than our dust limit {}",
+                               context.holder_dust_limit_satoshis
+                       )
+               })?;
                let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
                        post_channel_value_sat,
-                       their_dust_limit_satoshis,
+                       context.counterparty_dust_limit_satoshis,
                        prev_funding.holder_selected_channel_reserve_satoshis == 0,
                )
-               .map_err(|()| format!("The post-splice channel value {post_channel_value_sat} is smaller than their dust limit {their_dust_limit_satoshis}"))?;
+               .map_err(|()| {
+                       format!(
+                               "The post-splice channel value {post_channel_value_sat} is smaller \
+                               than their dust limit {}",
+                               context.counterparty_dust_limit_satoshis,
+                       )
+               })?;

                Ok(Self {
                        channel_transaction_parameters: post_channel_transaction_parameters,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Goal: Merge

Development

Successfully merging this pull request may close these issues.

4 participants